一线架构师带你玩性能优化
系统优化一个方面是系统化的对IT系统或交易链上的每个环节进行分析并优化,另一个是对单一系统进行瓶颈点分析和调优。但优化的目标大致相同,无非是提高系统的响应速度、吞吐量、降低各层耦合,以应对灵活对边的市场。
系统优化的3个层次:IT架构治理层、系统层、基础设施层。
IT系统治理层:优化的目的不只是性能优化,还会有为适应业务架构变化而带来的应用架构优化(如:应用分层、服务治理等)。
系统层:优化的目的包括业务流程优化、数据流程优化(如:提高系统负载、减少系统开销等)
基础设施层:优化的目的主要是提高IAAS平台的能力(如:建立弹性集群具备横向扩展能力,支持资源快速上下线和转移等)。
什么是方法论,我个人的理解就是听起来很牛,做过的人认为是废话,但可以指明行动方向或持续改进的东西。
2.1 常用方法论
(1)不访问不必要的数据——减少交易线上不必要的环节,减少故障点和维护点。
(2)就近加载/缓存为王——减少不必要的访问。
(3)故障隔离——不要因为一个系统瓶颈压垮整个交易平台。
(4)具备良好的扩展能力——合理的利用资源、提高处理效率和避免单点故障。
(5)对交易链进行优化提高吞吐量——异步/减少串行、合理拆分(垂直/水平拆分)、规则前置。
(6)性能和功能同等重要——交易链上5个性能变为设计阶段90%后为则整体性能为设计时的59%。
2.2 优化的一般思路
2.3 优化的原则
在应用系统的设计、开发过程用中,应始终把性能放在考虑的范围内。
确定清晰明确的性能目标是关键。
性能调优是伴随整个项目周期的,最好进行分阶段设定目标开展,在达到预期性能目标之后即可对本阶段工作进行总结和知识转移进入下一阶段调优工作。
必须保证调优后的程序运行正确。
性能更大程度是取决于良好的设计,调优技巧只是一个辅助手段。
调优过程是叠代渐进的过程,每次调优的结果要反馈到后续的代码开发中去。
性能调优不能以牺牲代码的可读性和维护性为代价。
3.1 常见性能问题
3.1.1常见客户端性能问题
加载慢:第一次启动慢或者重新加载慢;
无响应:事件出发后页面假死;
受网络带宽影响严重:因为需要下载大量资源文件,在一些在网络环境不好的地区页面;
JS内存溢出:频繁对对象的属性进行操作造成内存大量占用最终溢出。
3.1.2常见的J2EE 系统性能问题
内存泄漏:在运行过程中内存不断被占用而不能被回收,内存使用率随时间或负载的增加呈线性增长,系统处理效率随着时间或并发的增加而下降,直至将分配给JVM 的内存用尽而宕机,或重启后系统短时间内可恢复正常。
资源泄露:在将资源打开后未关闭或未成功关闭的问题。这些资源包括数据源连接,文件流等。当这些资源经常被打开而未能成功关闭,就会导致资源泄漏。数据连接泄漏就是常见的资源泄漏问题。
过载:系统过度使用,超出系统所能承受的负荷。
内部资源瓶颈:资源过度使用或分配不足引起资源瓶颈。
线程阻塞、线程死锁:线程退回到无法完成的同步点造成通信阻塞。
应用系统响应慢:由于应用本身或SQL不合理的问题,导致响应时间长。
应用系统不稳定,时快时慢的现象发生。
应用系统各种各样异常情况发生:有些是中间件服务器抛出的异常、有些是数据端抛出的异常。
3.1.3常见的数据库问题
死锁:因为请求保持或者执行效率低不能及时释放导或因为循环等待致表死锁;
IO繁忙:因为不良SQL或业务逻辑设计不合理导致大量IO等待;
CPU使用率居高不下:高并发或缓存穿透导致数据库CPU居高不下或忽高忽低。
3.2 调优的具体工作
天下武功为快不破,首要的就是提高系统的响应时间(响应时间 = 服务处理时间 + 排队时间),如经典的响应时间曲线所示,我们要做的就是通过程序优化减少服务响应时间,通过提高系统的吞吐量减少系统的排队时间。
响应时间曲线(摘自《Oracle性能预测》)
纵轴是响应时间。响应时间是服务时间和排队时间的总和。横轴是到达率。随着每单位时间进入系统事务数的递增,曲线随之向右滑动。随着到达率的继续增加,在某一时候,排队时间将陡然上升。当这种情况发生时,响应时间也将陡然上升,性能下降,而用户感到非常沮丧。
下面通过以往项目中的案例来分析性能优化的具体工作。
3.2.1交易线优化
交易线是从服务的消费者为出发点,看交易在各个层面应该完成的功能,以及功能点之间的关系。功能点之间的关系用有向路径来表示:
交易线优化的原则:
最短路径:减少不必要的环节,避免故障点;
交易完整性:通过冲正或补偿交易等确保交易线各环节的事物一致性;
故障隔离和快速定位:屏蔽异常情况对正常交易的影像,通过交易码或错误码能快速等位问题;
流量控制原则:可以通过对服务通道进行流量控制,并结合优先级设置优先处理级别高的业务;
超时控制漏斗原则:尽量保持交易线上前端系统超时设置应该大于后端系统。
【案例】随着架构的演变,过去一站是构建的竖井式系统,逐步发展为现在的以服务为单元可灵活构建的独立单元:
在服务治理的过程中原来的核心业务系统被打碎为各种独立的业务组件,一些中间层平台型系统基于这些业务组件和流程服务逐渐构建了业务服务,并成为前端应用的快速构建提供业务支撑。在这个过程中服务识别和构建是基础,交易线的规范是保障,通过交易线规范可以确定服务治理有所为,有所不为,这是因为随软件版本迭代,很少有个人能把系统的全部细节都考虑清楚,所以要以规则治理,而不是人治。
要开发一个订单查询功能A,服务整合平台的B和C两个服务都可以完成相同功能,只是B在C的基础上增加了一些额外不需要的校验,按照最短路径原则这个时候A应该直接调用C服务。
当服务提供者D处理能力不足时,应该及时通知服务消费者C或者按照优先级丢弃部分访问通道的请求,前端消费者接收后端流量控制错误码并及时通知用户。这样可以避免在系统达到容量限制后,所有用户级别都被拒绝服务。流量控制的目的之一是保证各系统健康稳定运行。一般使用计数器按照交易类型来检测交易的并发数,不同交易类型,使用不同计数器。当交易请求到达时,计数器加1,当请求响应或者超时,计数器减1。
3.2.2客户端优化
客户端优化的首要目标是加快页面展现速度,其次是减少对服务端的调用。
常见解决办法:
分析瓶颈点,有针对性优化;
缓存为王,通过在客户端缓存静态数据提升页面响应时间;
通过GZIP压缩减少客户端网络下载流量;
使用压缩工具对js进行压缩,减少js文件大小;
删除、合并脚本、样式表及图片减少get请求;
无阻塞加载JS
预加载(图片、css样式、js脚本);
按需加载js脚本;
优化js处理方法提升页面处理速度。
WEB请求时序图:
【案例】下面是某企业内部应用系统客户端HTTP请求监控记录:
从上图中可以看到共计发送25次请求(21次命中缓存、4次与服务端交互)。
从统计信息可以看到:总计请求耗时5.645秒,进行4次网络交互,接收5.9KB数据。发送110.25KB数据,GZIP压缩节省了:8KB数据。
后来该页面通过优化后端请求、合并和压缩JS/JSP文件等将页面响应时间优化到2秒左右。
PS:前端优化最好了解浏览器原理、HTTP原理
3.2.3服务端优化
【案例】记一次资源泄露,具体表现为RESULT-SET未关闭:
RESULT-SET未关闭统计
根据堆栈跟踪日志查看应用程序发现程序代码存在只关闭connection未关闭Statement和ResultSet的问题。
针对关闭connection是否会自动关闭Statement和ResultSet的问题,以及Statement和ResultSet所占用资源是否会自动释放问题,JDBC处理规范或JDK规范中做了如下描述:
JDBC处理规范
JDBC. 3.0 Specification——13.1.3 Closing Statement Objects
An application calls the method Statement.close toindicate that it has finished processing a statement. All Statement objectswill be closed when the connection that created them is closed. However, it isgood coding practice for applications to close statements as soon as they havefinished processing them. This allows any external resources that the statementis using to be released immediately.
Closing a Statement object will close and invalidateany instances of ResultSet produced by that Statement object. The resourcesheld by the ResultSet object may not be released until garbage collection runsagain, so it is a good practice to explicitly close ResultSet objects when theyare no longer needed.
These comments about closing Statement objects applyto PreparedStatement and CallableStatement objects as well.
JDBC. 4.0 Specification——13.1.4 Closing Statement Objects
An application calls the method Statement.close toindicate that it has finished processing a statement. All Statement objectswill be closed when the connection that created them is closed. However, it isgood coding practice for applications to close statements as soon as they havefinished processing them. This allows any external resources that the statementis using to be released immediately.
Closing a Statement object will close and invalidateany instances of ResultSet produced by that Statement object. The resourcesheld by the ResultSet object may not be released until garbage collection runsagain, so it is a good practice to explicitly close ResultSet objects when theyare no longer needed.
Once a Statement has been closed, any attempt toaccess any of its methods with the exception of the isClosed or close methodswill result in a SQLException being thrown.
These comments about closing Statement objects applyto PreparedStatement and CallableStatement objects as well.
规范说明:connection.close 自动关闭 Statement.close 自动导致 ResultSet 对象无效(注意只是 ResultSet 对象无效,ResultSet 所占用的资源可能还没有释放)。所以还是应该显式执行connection、Statement、ResultSet的close方法。特别是在使用connection pool的时候,connection.close 并不会导致物理连接的关闭,不执行ResultSet的close可能会导致更多的资源泄露。
JDK处理规范:
JDK1.4
Note: A ResultSet object is automatically closed by theStatement object that generated it when that Statement object is closed,re-executed, or is used to retrieve the next result from a sequence of multipleresults. A ResultSet object is also automatically closed when it is garbagecollected.
Note: A Statement object is automatically closed when it isgarbage collected. When a Statement object is closed, its current ResultSetobject, if one exists, is also closed.
Note: A Connection object is automatically closed when it is garbagecollected. Certain fatal errors also close a Connection object.
JDK1.5
Releases this ResultSet object's database and JDBC resources immediatelyinstead of waiting for this to happen when it is automatically closed.
Note: A ResultSetobject is automatically closed by the Statement object that generated it whenthat Statement object is closed, re-executed, or is used to retrieve the nextresult from a sequence of multiple results. A ResultSet object is alsoautomatically closed when it is garbage collected.
规范说明:
1.垃圾回收机制可以自动关闭它们;
2.Statement关闭会导致ResultSet关闭;
3.Connection关闭不一定会导致Statement关闭。
现在应用系统都使用数据库连接池,Connection关闭并不是物理关闭,只是归还连接池,所以Statement和ResultSet有可能被持有,并且实际占用相关的数据库的游标资源,在这种情况下,只要长期运行就有可能报“游标超出数据库允许的最大值”的错误,导致程序无法正常访问数据库。
针对该类问题建议:
(1) 显式关闭数据库资源,尤其是使用Connection Pool的时候;
(2) 最优经验是按照ResultSet,Statement,Connection的顺序执行close;
(3) 为了避免由于java代码有问题导致内存泄露,需要在rs.close()和stmt.close()后面一定要加上rs = null和stmt = null,并做好异常处理;
(4) 如果一定要传递ResultSet,应该使用RowSet,RowSet可以不依赖于Connection和Statement。
3.2.4JVM优化
针对JVM的参数调整是需要谨慎处理的。常见的JVM参数:
heap参数设置
-server –Xmx1G –Xms1G -Xmn512M-XX:PermSize=512M -XX:MaxPermSize=512M -XX:+UseCompressedOops
-server:选择"server" VM,一定要作为第一个参数,与之相对的参数是-client,"client" VM,增加-server参数会影响jvm的其他参数默认值。HotSpot包括一个解释器和两个编译器(client 和 server,二选一的),解释与编译混合执行模式,默认启动解释执行。server启动慢,占用内存多,执行效率高,适用于服务器端应用,JDK1.6以后在具有64位能力的jdk环境下将默认启用该模式; client启动快,占用内存小,执行效率没有server快,默认情况下不进行动态编译,通常用于客户端应用程序或者PC应用开发和调试。
PS:据报道Hotspot的某些版本Servermode被报告有稳定性问题,因此jvm采用server mode还是client mode 需要通过长时间系统监测来评测。
垃圾回收参数设置
-XX:+DisableExplicitGC-XX:+UseParNewGC-XX:+UseConcMarkSweepGC-XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled
-XX:+DisableExplicitGC禁止System.gc(),免得程序员误调用gc方法影响性能;
PS:根据历史经验一般垃圾回收时间占比小于2%则认为对性能影响不大。
日志类参数
-XX:+PrintClassHistogram -XX:+PrintGCDetails-XX:+PrintGCTimeStamps-Xloggc:log/gc.log
-XX:+ShowMessageBoxOnError-XX:+HeapDumpOnOutOfMemoryError-XX:+HeapDumpOnCtrlBreak
调试的时候设置一些日志参数,如-XX:+PrintClassHistogram -XX:+PrintGCDetails-XX:+PrintGCTimeStamps -Xloggc:log/gc.log,这样可以从gc.log里查看gc频繁程度,根据此来评估对性能的影响。
调试的时候设置异常宕机时产生heap dump文件,-XX:+ShowMessageBoxOnError-XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpOnCtrlBreak,这样可以查看宕机时系统执行哪些操作。
性能监控类参数设置
-Djava.rmi.server.hostname=Server IP-Dcom.sun.management.jmxremote.port=7091-Dcom.sun.management.jmxremote.ssl=false-Dcom.sun.management.jmxremote.authenticate=false
增加以上参数既可以通过visualVM或jconsole监控远程JVM的执行情况。
JVM参数调整
调整heap参数和垃圾回收参数,需要通过压力测试和监控记录综合分析最有方案:
Id | 参数组合 | TransResponse Time | Throughput | Passed Transactions | |
Heap参数 | GC参数 | ||||
1 | |||||
2 | |||||
3 | |||||
4 | |||||
5 |
【案例】应用服务器运行一段Object实例数量达百万/千万级别,使用IBMHeapAnalyzer分析内存溢出时生成heapdump文件,发现89.1%的空间被基础对象占用(为从数据库加载大量记录导致):
使用jprofiler监控后发现,大量未释放的VchBaseVo对象:
查看工程代码,发现使用Hibernate的list()方法去查询,hibernatelist()方法优先查询缓存数据,如获取不到则从数据库中进行获取,从数据库获取到后Hibernate将会相应的填充一级、二级缓存,所以在应用服务器级别内存中出现百万级的对象占用内存问题,此为hibernate缓存的一个有效解决方案,但是在此处确实带来了性能问题,需要调用clear() 释放一级缓存占用的内存资源。
3.2.5数据库优化
【案例】某企业内部核心业务系统数据库出现业务高峰CPU使用率居高不下,存在大数据量查询、多表连接造成查询性能下降、表索引建立不合理等问题,最终通过以下办法将业务高峰期CPU使用率控制在30%内:
在SQL*PLUS下执行下面语句:
SQL> set line 1000 --设置每行显示1000个字符 SQL> set autotrace traceonly --显示执行计划和统计信息,但是不显示查询输出 |
执行效率低下SQL语句:
select variablein0_.TOKENVARIABLEMAP_ as TOKENVAR7_1_ | |
from JBPM_VARIABLEINSTANCE variablein0_ | |
where variablein0_.TOKENVARIABLEMAP_ = '4888804' |
查看优化前的执行计划:
执行计划 |
---------------------------------------------------------- |
Plan hash value: 3971367966 |
------------------------------------------------------------------------------------------- |
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time| |
------------------------------------------------------------------------------------------- |
| 0 | SELECT STATEMENT | | 12 | 612 | 12408 (2)| 00:02:29 | |
|* 1 | TABLE ACCESS FULL| JBPM_VARIABLEINSTANCE | 12 | 612 | 12408 (2)| 00:02:29 | |
------------------------------------------------------------------------------------------- |
Predicate Information (identified by operation id): |
--------------------------------------------------- |
1 - filter("VARIABLEIN0_"."TOKENVARIABLEMAP_"=4888804) |
统计信息 |
---------------------------------------------------------- |
1 recursive calls |
1 db block gets |
48995 consistent gets |
48982 physical reads |
0 redo size |
1531 bytes sent via SQL*Net to client |
248 bytes received via SQL*Net from client |
2 SQL*Net roundtrips to/from client |
0 sorts (memory) |
0 sorts (disk) |
9 rows processed |
从执行计划看该语句缺少索引导致全表扫描。消耗总一致性读占用为:48995,平均每行一致性读:48995/9=5444,物理读为:48982,不满足正常性能需要。创建索引优化后的执行计划:
统计信息 |
---------------------------------------------------------- |
1 recursive calls |
0 db block gets |
6 consistent gets |
4 physical reads |
0 redo size |
1530 bytes sent via SQL*Net to client |
248 bytes received via SQL*Net from client |
2 SQL*Net roundtrips to/from client |
0 sorts (memory) |
0 sorts (disk) |
9 rows processed |
从执行计划看该语句消耗总一致性读占用为:6,平均每行一致性读:6/9=0.67,物理读为:4,为比较高效的SQL。
一般认为平均每行一致性读超过100的为执行效率比较低的SQL,10以内为执行效率比较高的SQL。
根据以往优化实践,引起SQL效率低下的问题主要集中在如下几个方面:
(1)访问路径,主要集中在由于索引缺失或者数据迁移导致索引失效引起的SQL执行时无法使用索引扫描,而被迫使用全表扫描访问路径。此时的解决方法是建立缺失的索引或者重建索引。
(2)过度使用子查询,在某些情形下我们会连接多个大表,而此时由于业务逻辑的需要我们经常会使用到某些子查询,由于语句的逻辑太过复杂,致使oracle无法自动将子查询语句转换为多表连接操作,由此带来的结果是导致oracle选择错误的执行路径,带来语句执行性能的急剧下降。因此,我们需要尽可能使用连接查询代替子查询,这样可以帮助oracle查询优化器根据数据分区情况、索引设计情况,选择合理的连接顺序、连接技术以及表访问技术,即选择最高效的执行计划。
(3)使用绑定变量的好处是可以避免硬解析,好处在此不多谈,但带来的坏处是有可能选择错误的执行计划,而这有可能引起性能的急剧下降。目前oracle 10g中已经引入绑定变量分级机制来着手处理这个问题, 11g通过创建新的子游标而维护一个新的执行计划。在11g下我们可以大胆地使用绑定变量。
3.2.6负载均衡优化
负载均衡负责访问流量分发并提高系统横向扩展能力,避免系统单点故障。下面是某个项目组负载均衡问题分析和优化思路:
负载均衡算法:
随机(Random):即从pool地址里随机选择一台,好处:算法简单、性能高,请求耗时差别不大时能基本保持后端是均衡的;缺点:如果请求耗时差别较大那么后端机器容易不均衡。
Round-Robin:根据pool地址列表顺序选择,好处:算法简单、性能高,缺点:和随机一样如果请求耗时差别较大那么后端机器容易不均衡。
按权重:可以给pool中的主机分配权重,之后按照权重分配请求,好处:可以利旧特别是运行多年生产环境积累了不同配置的主机时需要此算法,但随着虚拟化该问题已经在IAAS层解决了。
Hash:即对请求信息做hash后分派到pool中的机器上(一般对静态资源的加载使用),好处:增加缓存命中率;缺点:因为需要读取请求信息并做hash,所以需要消耗更多的CPU资源。
按照响应时间:按照响应时间来分配,好处:可以将请求分配给性能好的主机;缺点:如果请求耗时差别较大那么后端机器容易不均衡。
按照最小连接数:根据主机连接数多少来分配,好处:均衡请求资源;缺点:新增服务器或重启某一台会因为瞬间请求量过大而出现性能问题。
会话保持:
无会话保持:每次请求当认为新的请求重新按照负载均衡算法分配给后端主机。好处:简单、性能高;缺点:需要后端服务做无状态处理;
基于接入ip保持:同一个IP第一次按照负载均衡算法分配后,第二次请求还是分配给上次的主机,好处:回话保持比较稳定;缺点:导致部分网络内用户都连入一台服务器;
基于cookie保持:第一次请求负载均衡器在HTTP请求头部insert cookie,第二次请求根据请求的HTTP头中的cookie分配给上次的主机。好处:相对稳定、可以灵活切换;缺点:偶尔因为清除cookie导致回话丢失。
健康检查:
基于TCP端口:监听端口是否启用,如果未监听到则将该主机冲pool中剔除,好处:简单、缺点:有可能容器启动、应用未启动就有请求分发过来
基于Http get/TCP请求:定期向服务器发送请求并判断的返回串与约定的是否一致,如果不一致则将该主机冲pool中剔除,好处:可以精准确定应用是否正常启动,可以动态控制服务是否在线,缺点:需要编写脚本。